ViewBinding,您真的理解了吗? | 开发者说·DTalk
The following article is from Android开发那点事儿 Author 不说谎的匹诺槽
具体废弃的原因我猜测:
不兼容 Java。虽然现在 Google 各种新技术都在以 java 为主,像协程,Compose 之类,但是这些都是可以独立于平台的,而控件绑定这个功能是基于平台的,必然需要考虑 java 用户群体;
类型安全: res 下的任何 id 都可以被访问,有可能因访问了非当前 Layout 下的 id 而出错;
虽然 kotlin-android-extensions 我们使用起来非常爽,但是从它的实现原理也暴露出来一些问题,在无形之中降低了程序的运行效率。
使用
ViewBinding 的简单使用可以说是非常简单。
首先在我们的 moudle 的 build.gradle 下进行配置:
buildFeatures {
viewBinding true
}
vViewbinding 的配置是独立于 moudle 的。
配置之后,会生成对应的 Binding 类,我们直接调用,进行绑定即可。
lateinit var binding: MainActivityBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = MainActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.message.text = "Android开发那点事儿"
}
原理
生成的 binding 文件是在 build/generated/data_binding_base_class_source_out/debug/out 目录下,我们先看下生成的类内容。
这是我定义的 XML 布局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MainFragment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
public final class MainActivityBinding implements ViewBinding {
private final ConstraintLayout rootView;
public final ConstraintLayout main;
public final TextView message;
private MainActivityBinding( ConstraintLayout rootView, ConstraintLayout main, TextView message) {
this.rootView = rootView;
this.main = main;
this.message = message;
}
@Override
@NonNull
public ConstraintLayout getRoot() {
return rootView;
}
@NonNull
public static MainActivityBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static MainActivityBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.main_activity, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
@NonNull
public static MainActivityBinding bind(@NonNull View rootView) {
int id;
missingId: {
ConstraintLayout main = (ConstraintLayout) rootView;
id = R.id.message;
TextView message = ViewBindings.findChildViewById(rootView, id);
if (message == null) {
break missingId;
}
return new MainActivityBinding((ConstraintLayout) rootView, main, message);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
@Override
public void setContentView(View v) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
contentParent.addView(v);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
原来是由 actiivty 进行布局解析,由我们自己进行控件绑定并使用,现在就相当于 ViewBinding 将这两件事情都做了。
那这些 binding 类是如何生成的呢?这就是接下来我们要探索的话题。
当我改变布局文件的时候,发现 binding 类文件并不会实时发生改变,需要编译之后 binding 类文件才会进行对应改变,由此推断,binding 类文件应该是 APG 在编译项目的时候生成的。
我们运行一下,看下 Task:
在 AS 右侧的 gradle 任务栏中,找到了关于 databinding 的 task。
我们再分别执行 dataBindingMergeDependencyArtifactsDebug,dataBindingMergeGenClassesDebug 和 dataBindingGenBaseClassesDebug。
先 clean 操作,然后执行前两个 task 之后,发现只是生成了两个空文件夹,并未有内容生成:
那 dataBindingMergeGenClassesDebug 这个 task 就是我们要探索的重点。
接下来我们引入 AGP 源码和 Databinding 源码,进行分析:
implementation 'com.android.tools.build:gradle:7.0.2'
implementation 'androidx.databinding:databinding-compiler-common:7.0.2'
implementation 'androidx.databinding:databinding-common:7.0.2'
implementation 'com.android.databinding:baseLibrary:7.0.2'
在 app 的 build.gradle 中引入就行,我们在 External Libraries 中进行查阅。
我们查找的入口主要有两个,一个 AGP 的 TaskManager 类,这个是管理 Task 创建的类,还有另外一个入口,就是从 /com/android/build/gradle/internal/tasks 路径去找,这个是属于熟能生巧的一个捷径。
首先看 TaskManager,通过方法查阅,我们会发现关于创建 DataBinding Task 的只要一个方法 createDataBindingTasksIfNecessary。
protected fun createDataBindingTasksIfNecessary(creationConfig: ComponentCreationConfig) {
val dataBindingEnabled = creationConfig.buildFeatures.dataBinding
val viewBindingEnabled = creationConfig.buildFeatures.viewBinding
if (!dataBindingEnabled && !viewBindingEnabled) {
return
}
taskFactory.register(DataBindingMergeBaseClassLogTask.CreationAction(creationConfig))
taskFactory.register(
DataBindingMergeDependencyArtifactsTask.CreationAction(creationConfig))
DataBindingBuilder.setDebugLogEnabled(logger.isDebugEnabled)
taskFactory.register(DataBindingGenBaseClassesTask.CreationAction(creationConfig))
// DATA_BINDING_TRIGGER artifact is created for data binding only (not view binding)
if (dataBindingEnabled) {
if (projectOptions[BooleanOption.NON_TRANSITIVE_R_CLASS]
&& isKotlinKaptPluginApplied(project)) {
// TODO(183423660): Undo this workaround for KAPT resolving files at compile time
taskFactory.register(MergeRFilesForDataBindingTask.CreationAction(creationConfig))
}
taskFactory.register(DataBindingTriggerTask.CreationAction(creationConfig))
setDataBindingAnnotationProcessorParams(creationConfig)
}
}
这里首先获取配置信息,看看 ViewBinding 和 DataBinding 的开关状态,如果两个都是关闭直接返回。否则进行 Task 注册,在这里进行注册的 task,有两个是我们在编译过程中看到的 task,再继续,就是当 databinding 开启的时候,会再额外注册 task,通过这里我们可以了解到 viewBinding 只是 DataBinding 中的一部分功能。viewbinding 只是进行控件绑定,DataBinding 除了基础的控件绑定之外,还拥有双向数据绑定等功能。
接下来我们看看 DataBindingGenBaseClassesTask。
这个 Task 类的路径是 com.android.build.gradle.internal.tasks.databinding,跟我提到的第二个入口吻合,所以以后分析 AGP 源码,可以从这个路径来找对应的 Task,这是一种取巧的方式。
写过自定义插件的朋友都知道,自定义 Task 中需要用注解 @TaskAction 来标识一下 task 的运行入口。
@TaskAction
fun writeBaseClasses(inputs: IncrementalTaskInputs) {
recordTaskAction(analyticsService.get()) {
val args = buildInputArgs(inputs)
CodeGenerator(
args,
sourceOutFolder.get().asFile,
Logger.getLogger(DataBindingGenBaseClassesTask::class.java),
encodeErrors,
collectResources()).run()
}
}
可以看到 writeBaseClasses 方法被 @TaskAction 注解标识,那么这就是我们分析的入口。
这里主要是创建了 CodeGenerator 类,然后执行了 run() 方法。
override fun run() {
try {
initLogger()
BaseDataBinder(
LayoutInfoInput(args),
if (symbolTables != null) this::getRPackage else null)
.generateAll(DataBindingBuilder.GradleFileWriter(sourceOutFolder.absolutePath))
} finally {
clearLogger()
}
}
fun generateAll(writer : JavaFileWriter) {
..............
layoutBindings.forEach { layoutName, variations ->
...........
if (variations.first().isBindingData) {
check(input.args.enableDataBinding) {
"Data binding is not enabled but found data binding layouts: $variations"
}
val binderWriter = BaseLayoutBinderWriter(layoutModel, libTypes)
javaFile = binderWriter.write()
classInfo = binderWriter.generateClassInfo()
} else {
check(input.args.enableViewBinding) {
"View binding is not enabled but found non-data binding layouts: $variations"
}
val viewBinder = layoutModel.toViewBinder()
javaFile = viewBinder.toJavaFile(useLegacyAnnotations = !useAndroidX)
classInfo = viewBinder.generatedClassInfo()
}
.................
}
这里对代码进行了精简,这里首先做了一个判断,判断是否为 DataBinding,很明显我们需要分析的内容在 else 里面。
在 else 里面,先判断了 viewBinding 是否开启,然后将 BaseLayoutModel 对象转化为了 ViewBinder 对象,接下来执行了 ViewBinder 的拓展方法 toJavaFile,这个方法名的意思就很明显了,是去转化为 Java 文件的。
fun ViewBinder.toJavaFile(useLegacyAnnotations: Boolean = false) =
JavaFileGenerator(this, useLegacyAnnotations).create()
fun create() = javaFile(binder.generatedTypeName.packageName(), typeSpec()) {
addFileComment("Generated by view binder compiler. Do not edit!")
}
private fun typeSpec() = classSpec(binder.generatedTypeName) {
增加 public final 修饰
addModifiers(PUBLIC, FINAL)
实现 ViewBinding 接口
addSuperinterface(ClassName.get(viewBindingPackage, "ViewBinding"))
添加 rootView 变量
addField(rootViewField())
添加 控件 变量
addFields(bindingFields())
创建 无参构造方法
addMethod(constructor())
创建 根布局的 get方法
addMethod(rootViewGetter())
if (binder.rootNode is RootNode.Merge) {
addMethod(mergeInflate())
} else {
创建一个参数的 inflate 方法
addMethod(oneParamInflate())
创建三个参数的 inflate 方法
addMethod(threeParamInflate())
}
添加 bind 方法
addMethod(bind())
}
使用过 javapoet 的同学可以看出这就是使用 javapoet 来创建 java 文件。经过这些创建流程与我们生成的 viewbinding 类文件对比,可以发现完全吻合。所以我们的 ViewBinding 类文件就是在这里通过 javapoet 生成的。
总结
最后总结一下:
我们通过观察编译流程,得出 dataBindingGenBaseClassesDebug 是生成 binding 类的 task,然后通过 TaskManager 找到对应的 DataBindingGenBaseClassesTask,通过 @TaskAction 注解找到 task 执行的入口,最后调用到 DataBinding 里面 BaseDataBinder 类,在这个过程中,通过 ViewBinder 调用到了 JavaFileGenerator 的 create() 方法,在这里通过 javapoet 生成了我们所使用 Viewbinding 类。
整体调用流程:
TaskManager
->writeBaseClasses
->CodeGenerator :: run()
->BaseDataBinder::generateAll()
->ViewBinder::toJavaFile()
->JavaFileGenerator:: create()
->typeSpec()
->javapoet
整体流程并不算复杂,大家在阅读后最好还是自己去跟一遍源码,这个亲自跟一遍,自己理解的才算透彻。
理论配合实践,才能真正学会。
长按右侧二维码
查看更多开发者精彩分享
"开发者说·DTalk" 面向